﻿/* 
 * Subscribe to all the channels. Subscriptions can be wildcards e.g.
 * "/foo/bar"   : Subscribes to the channel /foo/bar only
 * "/foo/bar/*"  : Subscribes to the channel all the children of /foo/bar/ e.g. /foo/bar/baz, /foo/bar/buz but not /foo/bar/baz/boo
 * "/foo/bar/**" : Subscribes to all the descentends of /foo/bar/ e.g. /foo/bar/baz, /foo/bar/baz/boo, /foo/bar/bar/baz/boo
 * Below, we shall receive all the messages to all the channels
 *
 * EventBroker.subscribe("/**", function(objMsg, strChannel) {
 *   alert(strChannel + ": " + objMsg);
 * });
 * 
 * To publish a message, just call the EventBroker's publish method like so:
 * EventBroker.publish("/foo/bar", {
   name: "MessageName",
   messageHeader: "This is a header string but can be any object",
   body: "This is body but can be any arbitrary object"
});
 */

package extremefx.events {
	import extremefx.conditions.requiresString;	
	import extremefx.tools.ArrayUtils;
	import extremefx.tools.StringUtils;		

	/**
	 * @author Marcelo Volmaro
	 */
	public final class EventBroker {
		private static var _inst:EventBroker;
		
		private var _channels:Object;
		private var _patternSuscribers:Object;

		public function EventBroker(pRes:PrivateClass) {
			_patternSuscribers = {};
			_channels = {};
		}
		
		/**
		 * Publishes the message objMessage to channel pChannelName
		 * All the subscribers registered with this channel will get the message
		 * @param pChannelName The name of the channel to publish to
		 * @param pMessaje The message to publish
		 */
		public static function publish(pChannelName:String, pMessaje:EventArgs):void {
			_instance._publish(pChannelName, pMessaje);
		}
		
		/**
		 * Subscribes to specified channel. If the channel specified by pChannelName
		 * does not exist, it is created. Any message published to this channel will
		 * be delivered to pSuscriber who is the subscriber to this channel
		 * @param pChannelName The name of the channel. This can be a wildcard pattern like so: /some/channel/* or some/channel
		 * @param pSuscriber The callback function that will be called with message as the parameter
		 */
		public static function subscribe(pChannelName:String, pSuscriber:Function):void {
			_instance._subscribe(pChannelName, pSuscriber);
		}
		
		/**
		 * Unsubscribes to specified channel.
		 * @param pChannelName The name of the channel (can be a wildcard in which case pSuscriber from all the matching channels is removed)
		 * @param pSuscriber The callback function that was used when subscribing
		 */
		public static function unsubscribe(pChannelName:String, pSuscriber:Function):void {
			_instance._unsubscribe(pChannelName, pSuscriber);
		}
		
		private function _unsubscribe(pChannelName:String, pSuscriber:Function):void {
			if (_isPattern(pChannelName)){
				ArrayUtils.remove(_patternSuscribers[pChannelName], pSuscriber);
			}
			
			for (var cn:String in _channels){
				if (_matches(cn, pChannelName)){
					_channels[cn].remove(pSuscriber);
				}
			}
		}
		
		private function _subscribe(pChannelName:String, pSuscriber:Function):void {
			_checkChannelName(pChannelName);
			
			// check if this subscription is a pattern i.e. /some/name/** | some/name/*
			if (_isPattern(pChannelName)){
				if (_patternSuscribers[pChannelName]){
					_patternSuscribers[pChannelName].push(pSuscriber);
					
				} else {
					_patternSuscribers[pChannelName] = [pSuscriber];
				}
				
				for (var channel:String in _channels){
					if (_matches(pChannelName, channel)){
						_channels[pChannelName].add(pSuscriber);
					}
				}
				
				
			} else {
				var c:Channel = _channels[pChannelName];
				if (!c){
					c = _channels[pChannelName] = _createChannel(pChannelName);
				}
				
				c.add(pSuscriber);
			}
			
		}
		
		private function _publish(pChannelName:String, pMessaje:EventArgs):void {
			var c:Channel = _channels[pChannelName];
			if (!c){
				c = _channels[pChannelName] = _createChannel(pChannelName);
				_addPSuscriber(c);
			}
			
			c.publish(pMessaje);
		}
		
		
		private static function get _instance():EventBroker{
			if (_inst == null){
				_inst = new EventBroker(new PrivateClass());
			}
			
			return _inst;
		}

	    /**
	     * Creates a new Channel for the specified name. This method is always called
	     * after calling checkChannelName(name)
	     * @param pName theName The name for this channel. This should be of the form "/some/name"
	     * @return the newly created channel object
	     */
		
		private function _createChannel(pName:String):Channel {
			pName = StringUtils.trimEnd(pName, Vector.<String>(["/"]));
			return new Channel(pName);
		}
		
		/**
		 * Gets the segments of the specified channel name as array.
		 * e.g. If the channel name is specified as "/some/name",  an array containing
		 * two segments is returned like this: ["some", "name"]
		 */
		private function _getSegment(pName:String):Array {
			var s:Array = pName.split("/");
			if (s[0] == "") s.slice(0, 1);
			return s;
		}
		
		private function _checkChannelName(pName:String):void {
			requiresString(pName, "name").isNotNullOrEmpty().startsWith("/");
		}
		
		private function _isPattern(pName:String):Boolean {
			return StringUtils.endsWith(pName, "/**") || StringUtils.endsWith(pName, "/*");  
		}
		
		/**
		 * Determines if pName matches with channel name specified by
		 * pChannel. This method also supports wildcard pattern matching for two wildcards:
		 * "/*" For children and "/**" For descendents
		 * @return true if pName matches with pChannel
		 */
		
		private function _matches(pName:String, pChannel:String):Boolean {
			if (pName == pChannel) return true;
			
			if(!_isPattern(pName) || _isPattern(pChannel))  {
				return false;
			}
			
			var segs:Array = _getSegment(pName);
			var channelSegs:Array = _getSegment(pChannel);
			
			var depth:uint = segs.length;
			var channelDepth:uint = channelSegs.length;
			
			if(depth > channelDepth) {
				return false;
			}
			
			segs.splice(segs.length - 1, 1);
			depth = segs.length;
			
			var channelPart:String = channelSegs.slice(0, depth).join("/");
			var namePart:String = segs.join("/");
			
			if(channelPart != namePart)  {
				return false;
			}
			
			if (StringUtils.endsWith(pName, "/**")) return true;
			return (channelDepth - depth) == 1;
		}
		
		/**
		 * Adds any of the subscribers that are pattern subscribers to a Channel.
		 * This is called internally when a message is published to a channel and
		 * that channel does not exist yet. When this happens, a new Channel is created
		 * and any previously registered pattern subscribers are added to the channel
		 * @param pChannel The channel to which wildcard or pattern subscribers are added if they match.
		 */
		
		private function _addPSuscriber(pChannel:Channel):void {
			var pc:Object = _patternSuscribers;
			var name:String = pChannel.name;
			for (var pattern:String in pc){
				if (_matches(pattern, name)){
					var suscribers:Array = pc[pattern];
					if (suscribers){
						for each(var f:Function in suscribers){
							pChannel.add(f);
						}
					}
				}
			}
		}		
	}
}

import extremefx.events.EventArgs;

final class PrivateClass {}

/**
 * A Channel/Topic/Queue to which messages are published. Each channel is represented
 * by a path name of the form /some/channel/name which is hierarchical
 */
final class Channel {
	public var suscribers:Vector.<Function>;
	public var name:String;
	
	/**
	 * @param pName The name of this channel. Should be of the form /some/name
	 */
	public function Channel(pName:String) {
		name = pName;
		suscribers = new Vector.<Function>();
	}
	
	
	public function add(pSuscriber:Function):void {
		var idx:int = suscribers.indexOf(pSuscriber);
		if (idx == -1) {
			idx = suscribers.length;
		}
		
		suscribers[idx] = pSuscriber;
	}
	
	public function remove(pSuscriber:Function):void {
		var idx:int = suscribers.indexOf(pSuscriber);
		if (idx != -1) suscribers.splice(idx, 1);
	}
	
	public function publish(pMessage:EventArgs):void {
		var s:Vector.<Function> = suscribers.concat();
		for each (var f:Function in s){
			f(pMessage);
		}
	}
}